﻿using System;
using System.Collections.Generic;
using System.Text;
using NGinnBPM.Runtime.Events;
using NGinnBPM.Runtime.Messages;
using NGinnBPM.Lib.Interfaces;
using NGinnBPM.Lib.Schema;
using NGinnBPM.Lib.Data;
using NGinnBPM.MessageBus;
using NGinnBPM.Lib.Exceptions;
using NGinnBPM.Services;
using System.Reflection;
using NLog;
using System.Runtime.Serialization;
using System.ComponentModel;
using Newtonsoft.Json;
using NGinnBPM.Lib.Util;

namespace NGinnBPM.Runtime
{
    

    /// <summary>
    /// Status of task execution, used when scheduling tasks
    /// </summary>
    public enum TaskSchedulingStatus
    {
        /// <summary>Task ready for execution, can be 'kicked'</summary>
        Ready = 0,
        /// <summary>Task waiting for some event to occur or paused. Doesnt need kicking.</summary>
        Waiting = 1,
        /// <summary>Task execution is suspended due to an error</summary>
        Error = 2
    }
    
    /// <summary>
    /// Task instance information
    /// </summary>
    [Serializable]
    [DataContract]
    public abstract class TaskInstance :
        ITaskMessageHandler<CancelTaskMessage>,
        ITaskMessageHandler<FailTaskMessage>,
        ITaskMessageHandler<ForceCompleteTaskMessage>,
        ITaskMessageHandler<SelectTaskMessage>,
        ITaskMessageHandler<ITaskInstanceMessage>
    {
        private string _instId;
        private string _parentInstId;
        private string _procInstId;
        private TaskStatus _status;
        private string _taskId;
        private string _processDefinitionId;
        private TaskSchedulingStatus _scheduleStatus = TaskSchedulingStatus.Waiting;
        private DateTime _enabledDate;
        private DateTime? _selectedDate;
        private DateTime? _finishedDate;

        protected Logger log = LogManager.GetCurrentClassLogger();
        [NonSerialized]
        private ITaskContext _ctx;
        [NonSerialized]
        private ProcessDefinition _processDef;

        public TaskInstance()
        {
            TaskData = new Dictionary<string, object>();
        }
        /// <summary>
        /// Task instance ID
        /// </summary>
        [DataMember(IsRequired=true)]
        [TaskParameter(IsInput = false)]
        [Newtonsoft.Json.JsonProperty("id")]
        public string InstanceId
        {
            get { return _instId; }
            set 
            { 
                _instId = value;
            }
        }

        /// <summary>
        /// Parent process instance ID, or null if there's no parent process.
        /// </summary>
        [DataMember(IsRequired = false)]
        [TaskParameter(IsInput = false)]
        public string ProcessInstanceId
        {
            get { return _procInstId; }
            set { _procInstId = value; }
        }

        /// <summary>
        /// Parent task instance ID, or null if there's no parent task.
        /// </summary>
        [DataMember(IsRequired = false)]
        [TaskParameter(IsInput = false)]
        public string ParentTaskInstanceId
        {
            get { return _parentInstId; }
            set { _parentInstId = value; }
        }

        /// <summary>
        /// Date when the process instance was started.
        /// </summary>
        [DataMember(IsRequired=false)]
        public DateTime ProcessStartDate { get; set; }
        /// <summary>
        /// Current status
        /// </summary>
        [DataMember(IsRequired = true)]
        [TaskParameter(IsInput = false)]
        public TaskStatus Status
        {
            get { return _status; }
            set { _status = value; }
        }

        /// <summary>
        /// Current scheduling status. Meaningful only during task execution.
        /// This status is not persisted with task state. Why? externally maintained as for now.
        /// </summary>
        [DataMember(IsRequired = false)]
        public TaskSchedulingStatus SchedulingStatus
        {
            get { return _scheduleStatus; }
            set { _scheduleStatus = value; }
        }

        /// <summary>
        /// Extended task status information.
        /// In case of failure it contains error information.
        /// </summary>
        [DataMember(IsRequired=false)]
        public string StatusInformation { get; set; }

        /// <summary>
        /// Document identifier, used with processes
        /// that are constructed around external document
        /// </summary>
        [TaskParameter(IsInput = true, Required = false, DynamicAllowed = true)]
        [DataMember(EmitDefaultValue=false, IsRequired=false)]
        public string ExtDocumentId { get; set; }

        [TaskParameter(IsInput=true, Required=false, DynamicAllowed=true)]
        [DataMember(EmitDefaultValue=false, IsRequired=false)]
        public string DocRepository { get; set; }

        
        

        /// <summary>
        /// Current task data.
        /// </summary>
        [DataMember(IsRequired = false)]
        [JsonConverter(typeof(TaskDataJsonConverter))]
        public Dictionary<string, object> TaskData {get;set;}
        

        [DataMember(IsRequired = true)]
        public DateTime EnabledDate
        {
            get { return _enabledDate; }
            set { _enabledDate = value; }
        }

        /// <summary>
        /// Task selection date.
        /// </summary>
        [DataMember(IsRequired = false)]
        public DateTime? SelectedDate
        {
            get { return _selectedDate; }
            set { _selectedDate = value; }
        }
        
        /// <summary>
        /// Task completion/cancellation date
        /// </summary>
        [DataMember(IsRequired = false)]
        public DateTime? FinishedDate
        {
            get { return _finishedDate; }
            set 
            {
                if (!_selectedDate.HasValue)
                    _selectedDate = value;
                _finishedDate = value; 
            }
        }

        /// <summary>
        /// Id of task in process definition
        /// </summary>
        [DataMember(IsRequired = true)]
        [TaskParameter(IsInput = false)]
        public string TaskId
        {
            get { return _taskId; }
            set { _taskId = value; }
        }

        /// <summary>
        /// Id of process definition the task belongs to
        /// </summary>
        [DataMember(IsRequired = true)]
        public string ProcessDefinitionId
        {
            get { return _processDefinitionId; }
            set { _processDefinitionId = value; }
        }

        /// <summary>
        /// Parent process definition
        /// </summary>
        [DontSerialize]
        public ProcessDefinition ParentProcess
        {
            get { return _processDef; }
            set { _processDef = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="srcData"></param>
        protected void InitializeTaskParameters(Dictionary<string, object> srcData)
        {
            ITaskScript scr = this.GetScriptProviderForTask();
            scr.SourceData = srcData;
            Task tsk = ParentProcess.GetTask(TaskId);
            foreach (TaskParameterInfo tpi in RuntimeParameters)
            {
                if (tpi.IsInput)
                {
                    object val =  null;
                    VariableBinding vb = tsk.GetInputParameterBinding(tpi.Name);
                    if (vb != null)
                    {
                        try
                        {
                            if (vb.BindType == VariableBindingType.Expr)
                            {
                                val = scr.GetInputParameterValue(tpi.Name);
                            }
                            else if (vb.BindType == VariableBindingType.Literal)
                            {
                                val = vb.Expression;
                            }
                            else if (vb.BindType == VariableBindingType.CopyVar)
                            {
                                srcData.TryGetValue(vb.Expression, out val);
                            }
                        }
                        catch (Exception ex)
                        {
                            log.Error("Error setting parameter {0} of task {1}: {2}", tpi.Name, TaskId, ex);
                            throw new TaskParameterInvalidException(tpi.Name, string.Format("Invalid task input parameter: {0}", ex.Message)).SetInstanceId(InstanceId).SetTaskId(TaskId);
                        }
                    }
                    if (val == null && tpi.Required)
                        throw new TaskParameterInvalidException(tpi.Name, "missing required input parameter").SetInstanceId(InstanceId).SetTaskId(TaskId);
                    if (val != null)
                        SetParameterValue(tpi.Name, val);
                }
            }
        }

        /// <summary>
        /// Enable task. 
        /// You can override this method to add custom logic when task is enabled,
        /// but usually you will override OnTaskEnabled.
        /// </summary>
        /// <param name="inputData">Task input data</param>
        public virtual void Enable(Dictionary<string, object> inputData)
        {
            if (this.Status != TaskStatus.Enabling)
                throw new Exception("Invalid status");
            Task tsk = ParentProcess.RequireTask(TaskId);
            StructDef sd = tsk.GetInputDataSchema();
            ITaskScript scr = GetScriptProviderForTask();
            scr.SourceData = inputData;
            if (tsk.BeforeEnableScript != null && tsk.BeforeEnableScript.Length > 0)
            {
                scr.RunScriptBlock("BeforeEnableScript");
            }
            Dictionary<string, object> dob = new Dictionary<string, object>();
            
            foreach (VariableDef vd in tsk.Variables)
            {
                if (vd.VariableDir == VariableDef.Dir.In ||
                    vd.VariableDir == VariableDef.Dir.InOut)
                {
                    if (!inputData.ContainsKey(vd.Name))
                    {
                        if (vd.DefaultValueExpr != null && vd.DefaultValueExpr.Length > 0)
                        {
                            dob[vd.Name] = scr.GetDefaultVariableValue(vd.Name);
                        }
                        else if (vd.IsRequired)
                            throw new DataValidationException("Missing required input variable: " + vd.Name);
                    }
                    else
                        dob[vd.Name] = inputData[vd.Name];
                }
                else if (vd.VariableDir == VariableDef.Dir.Local || vd.VariableDir == VariableDef.Dir.Out)
                {
                    if (vd.DefaultValueExpr != null && vd.DefaultValueExpr.Length > 0)
                    {
                        dob[vd.Name] = scr.GetDefaultVariableValue(vd.Name);
                    }
                }
                TypeDef td = sd.ParentTypeSet.GetTypeDef(vd.TypeName);
                System.Diagnostics.Debug.Assert(td != null);
                if (dob.ContainsKey(vd.Name))
                {
                    dob[vd.Name] = vd.ConvertToValidMemberValue(sd.ParentTypeSet, dob[vd.Name]);
                }
            }
            sd = tsk.GetInternalDataSchema();
            DataUtil.Validate(dob, sd);
            InitializeTaskParameters(dob);
            TaskData = dob;
            EnabledDate = DateTime.Now;
            this.Status = TaskStatus.Enabled;
            AfterTaskInitialized();
            if (tsk.AfterEnableScript != null && tsk.AfterEnableScript.Length > 0)
            {
                scr.RunScriptBlock("AfterEnableScript");
            }
            AfterTaskEnabled();
        }

        /// <summary>
        /// Override this method to perform some actions
        /// when task is enabled. Warning: if you want to complete the task immediately
        /// override the AfterTaskEnabled method.
        /// </summary>
        protected virtual void AfterTaskInitialized()
        {
        }

        protected virtual void AfterTaskEnabled()
        {
        }

        /// <summary>
        /// Select task (start it!)
        /// </summary>
        public virtual void Select()
        {
            if (Status != TaskStatus.Enabled)
                throw new InvalidTaskStatusException(InstanceId, "Invalid status");
            this.Status = TaskStatus.Selected;
            SelectedDate = DateTime.Now;
            TaskSelected ev = new TaskSelected();
            ev.ParentTaskInstanceId = this.ParentTaskInstanceId;
            ev.SourceTaskInstanceId = this.InstanceId;
            Context.MessageBus.Notify(ev);
        }

        /// <summary>
        /// Force-complete the task.
        /// Warning: task will not complete if it's output data structure is incorrect or
        /// some out variables are missing.
        /// </summary>
        /// <param name="finishedBy">Id of person finishing the task. Optional, can be null.</param>
        /// <param name="updatedData">Task data to update before completion. Optional, can be null.</param>
        public virtual void Complete(string finishedBy, Dictionary<string, object> updatedData)
        {
            if (Status != TaskStatus.Enabled &&
                Status != TaskStatus.Selected)
                throw new InvalidTaskStatusException(InstanceId, "Invalid status");
            ExecuteOutputParamBindings(TaskData);
            if (updatedData != null)
                UpdateTaskData(updatedData);
            DefaultHandleTaskFinished(GetOutputData());
        }

        /// <summary>
        /// Get task output data
        /// </summary>
        /// <returns></returns>
        protected Dictionary<string, object> GetOutputData()
        {
            Task tsk = ParentProcess.GetTask(TaskId);
            Dictionary<string, object> outData = new Dictionary<string, object>();
            foreach (VariableDef vd in tsk.Variables)
            {
                if (vd.VariableDir == VariableDef.Dir.Out || vd.VariableDir == VariableDef.Dir.InOut)
                {
                    if (vd.IsRequired && !TaskData.ContainsKey(vd.Name))
                        throw new DataValidationException("Output variable not set: " + vd.Name).SetInstanceId(InstanceId).SetTaskId(TaskId).SetProcessDef(ProcessDefinitionId);
                    object v;
                    if (TaskData.TryGetValue(vd.Name, out v)) outData[vd.Name] = v;
                }
            }
            return outData;
        }

        /// <summary>
        /// Force-fail the task
        /// </summary>
        /// <param name="errorInformation"></param>
        public virtual void Fail(string errorInformation)
        {
            if (Status != TaskStatus.Enabled &&
                Status != TaskStatus.Selected)
                throw new Exception("Invalid status");
            DefaultHandleTaskFailure(errorInformation, true);
        }

        /// <summary>
        /// Put task in 'Failed' state and send failure notification
        /// to parent task.
        /// </summary>
        /// <param name="errorInfo"></param>
        protected virtual void DefaultHandleTaskFailure(string errorInfo, bool errorExpected)
        {
            log.Warn("Failing task: {0}", errorInfo);
            this.Status = TaskStatus.Failed;
            if (errorExpected)
                this.StatusInformation = errorInfo;
            else
                this.StatusInformation = string.Format("Unexpected error: {0}", errorInfo);
            FinishedDate = DateTime.Now;
            TaskFailed tfe = new TaskFailed(InstanceId, ParentTaskInstanceId, ProcessInstanceId, errorInfo);
            tfe.IsExpected = errorExpected;
            Context.MessageBus.Notify(tfe);
        }

        /// <summary>
        /// Cancel the task
        /// </summary>
        public virtual void Cancel()
        {
            lock (this)
            {
                if (Status == TaskStatus.Cancelled)
                {
                    log.Info("Task already cancelled.");
                    return;
                }
                else if (Status == TaskStatus.Cancelling)
                {
                    log.Info("Task already cancelling");
                    return;
                }
                else if (Status != TaskStatus.Enabled && Status != TaskStatus.Selected)
                {
                    log.Warn("Cancel task {0} ({1}): Invalid status when cancelling ({2}). Ignoring the call.", InstanceId, TaskId, Status);
                    return;
                }
                DefaultHandleTaskCancelled();
            }
        }

        

        /// <summary>
        /// Update internal task data.
        /// </summary>
        /// <param name="updatedData"></param>
        public virtual void UpdateTaskData(Dictionary<string, object> updatedData)
        {
            Task tsk = ParentProcess.GetTask(TaskId);
            StructDef sd = tsk.GetInternalDataSchema();
            Dictionary<string, object> dob2 = new Dictionary<string, object>(TaskData);
            foreach (VariableDef vd in tsk.Variables)
            {
                TypeDef td = sd.ParentTypeSet.GetTypeDef(vd.TypeName);
                if (updatedData.ContainsKey(vd.Name))
                {
                    object val = updatedData[vd.Name];
                    val = vd.ConvertToValidMemberValue(sd.ParentTypeSet, val);
                    dob2[vd.Name] = val;
                }
            }
            DataUtil.Validate(dob2, sd);
            TaskData = dob2;
        }

        public virtual void Activate(ITaskContext ctx)
        {
            _ctx = ctx;
            _processDef = ctx.ParentProcess;
        }

        public virtual void Passivate()
        {
            _ctx = null;
        }

        protected void RequireActivation(bool activated)
        {
            if (activated)
            {
                if (_ctx == null) throw new Exception();
            }
            else
            {
                if (_ctx != null) throw new Exception();
            }
        }

        [DontSerialize]
        protected ITaskContext Context
        {
            get { return _ctx; }
            set { _ctx = value; }
        }

        

        
        /// <summary>
        /// Execute output parameter bindings and store the results
        /// in 'target' object. Should be called on task completion.
        /// </summary>
        /// <param name="target"></param>
        protected virtual void ExecuteOutputParamBindings(Dictionary<string, object> target)
        {
            ITaskScript scr = this.GetScriptProviderForTask();
            scr.SourceData = this.TaskData;
            Task tsk = ParentProcess.GetTask(TaskId);
            foreach (VariableBinding vb in tsk.OutputParameterBindings)
            {
                object val;
                if (vb.BindType == VariableBindingType.Expr)
                    val = scr.EvalOutputParameterBinding(vb.VariableName);
                else if (vb.BindType == VariableBindingType.Literal)
                    val = vb.Expression;
                else if (vb.BindType == VariableBindingType.CopyVar)
                    val = GetParameterValue(vb.Expression);
                else throw new Exception();
                target[vb.VariableName] = val;
            }

        }
        /// <summary>
        /// Default handler for task completion.
        /// Will set completed status and send notification and output data to parent task
        /// But will not execute output parameter bindings (see 'Complete')
        /// </summary>
        protected virtual void DefaultHandleTaskFinished(Dictionary<string, object> outData)
        {
            Task tsk = ParentProcess.GetTask(TaskId);
            StructDef sd = tsk.GetOutputDataSchema();
            DataUtil.Validate(outData, sd);
            //outData.Validate(sd);
            FinishedDate = DateTime.Now;
            Status = TaskStatus.Completed;
            TaskCompleted tce = new TaskCompleted();
            tce.SourceTaskInstanceId = this.InstanceId;
            tce.ParentTaskInstanceId = this.ParentTaskInstanceId;
            tce.ProcessInstanceId = ProcessInstanceId;
            tce.OutputData = outData;
            Context.MessageBus.Notify(tce);
        }

        /// <summary>
        /// Default handler for task cancellation.
        /// Will set cancelled status and notify parent task.
        /// </summary>
        protected virtual void DefaultHandleTaskCancelled()
        {
            Status = TaskStatus.Cancelled;
            FinishedDate = DateTime.Now;
            TaskCancelled tc = new TaskCancelled();
            tc.SourceTaskInstanceId = InstanceId;
            tc.ParentTaskInstanceId = ParentTaskInstanceId;
            tc.ProcessInstanceId = ProcessInstanceId;
            Context.MessageBus.Notify(tc);
        }



        
        /// <summary>
        /// Persisted record version
        /// </summary>
        [DataMember(IsRequired = false)]
        [Newtonsoft.Json.JsonProperty("version")]
        public int PersistedVersion { get; set; }
        
        /// <summary>
        /// True if the task should be 'kicked' to continue.
        /// Atomic tasks should not override it - they return false.
        /// </summary>
        [DontSerialize]
        public virtual bool CanContinue
        {
            get { return false; }
        }

        /// <summary>
        /// Return task script provider for the task instance
        /// </summary>
        /// <returns></returns>
        protected virtual ITaskScript GetScriptProviderForTask()
        {
            ITaskScript tsc = Context.ScriptManager.GetTaskScript(Context.ParentProcess, this.TaskId);
            tsc.TaskInstance = this;
            tsc.TaskContext = Context;
            tsc.SourceData = this.TaskData;
            
;
            return tsc;
        }

        /// <summary>
        /// List of task instance runtime parameters
        /// </summary>
        [DontSerialize]
        public virtual IList<TaskParameterInfo> RuntimeParameters
        {
            get
            {
                List<TaskParameterInfo> lst = new List<TaskParameterInfo>();
                foreach (PropertyInfo pi in GetType().GetProperties())
                {
                    TaskParameterAttribute tpa = (TaskParameterAttribute) Attribute.GetCustomAttribute(pi, typeof(TaskParameterAttribute));
                    if (tpa != null)
                    {
                        TaskParameterInfo tpi = new TaskParameterInfo(pi.Name, pi.PropertyType, tpa.Required, tpa.IsInput, tpa.DynamicAllowed);
                        lst.Add(tpi);
                    }
                }
                return lst;
            }
        }

        /// <summary>
        /// Get runtime parameter value
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public virtual object GetParameterValue(string name)
        {
            PropertyInfo pi = GetType().GetProperty(name);
            if (pi == null) throw new TaskRuntimeException("Invalid property: " + name).SetTaskId(TaskId);
            return pi.GetValue(this, null);
        }

        /// <summary>
        /// Set runtime parameter value
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        public virtual void SetParameterValue(string name, object value)
        {
            PropertyInfo pi = GetType().GetProperty(name);
            if (pi == null) throw new TaskRuntimeException("Invalid property: " + name).SetTaskId(TaskId);
            try
            {
                object val = value;
                if (value != null)
                {
                    if (!(pi.PropertyType.IsAssignableFrom(value.GetType())))
                    {
                        TypeConverter tc = TypeDescriptor.GetConverter(pi.PropertyType);
                        if (tc == null) throw new TaskRuntimeException(string.Format("Cannot set value of task parameter {0} ({1}) -no suitable converter", pi.Name, pi.PropertyType)).SetInstanceId(InstanceId).SetTaskAndProcessDef(TaskId, ProcessDefinitionId);
                        val = tc.ConvertFrom(val);
                        //if (value is IConvertible)
                        //    val = Convert.ChangeType(value, pi.PropertyType);
                        //else
                        //{
                         //   
                        //}
                    }
                }
                pi.SetValue(this, val, null);
            }
            catch (Exception ex)
            {
                log.Error("Task {0}: Error setting value of property {1} to {2}: {3}", TaskId, name, value, ex);
                throw new TaskRuntimeException(string.Format("Invalid value for property {0}: {1}. Error: {2}", name, value, ex.Message)).SetTaskId(TaskId);
            }
        }

        /// <summary>
        /// Schedule a task completion timeout message
        /// </summary>
        protected void ScheduleInternalTimeout(string corrId)
        {
            ScheduleInternalTimeout(DateTime.Now.AddDays(3), corrId);
        }

        /// <summary>
        /// Schedule task completion timeout
        /// </summary>
        /// <param name="expiration"></param>
        protected void ScheduleInternalTimeout(DateTime expiration, string correlationId)
        {
            Context.MessageBus.NewMessage(new TaskInstanceTimeout { TargetTaskInstanceId = this.InstanceId })
                .SetDeliveryDate(expiration)
                .SetCorrelationId(correlationId)
                .Publish();
        }

        #region IMessageConsumer<CancelTaskMessage> Members

        public virtual void Handle(CancelTaskMessage message)
        {
            this.Cancel();
        }

        #endregion

        #region IMessageConsumer<FailTaskMessage> Members

        public virtual void Handle(FailTaskMessage message)
        {
            this.Fail(message.ErrorInformation);
        }

        #endregion

        #region IMessageConsumer<ForceCompleteTaskMessage> Members

        public virtual void Handle(ForceCompleteTaskMessage message)
        {
            this.Complete(message.CompletedBy, message.UpdatedData);
        }

        #endregion

        #region IMessageConsumer<SelectTaskMessage> Members

        public virtual void Handle(SelectTaskMessage message)
        {
            this.Select();
        }

        #endregion

        

        #region IMessageConsumer<ITaskInstanceMessage> Members

        public virtual void Handle(ITaskInstanceMessage message)
        {
            Type mType = message.GetType();

            Type tp = typeof(ITaskMessageHandler<>).MakeGenericType(mType);
            if (tp.IsAssignableFrom(this.GetType()))
            {
                MethodInfo mi = tp.GetMethod("Handle");
                mi.Invoke(this, new object[] { message });
                return;
            }
            else
            {
                log.Info("Unexpected message to a task: {0}", message);
                throw new Exception("Will not handle this message");
            }
        }

        #endregion
    }
}
